/*            Copyright(c) 2006-2008 Dave Sexton           *
 * http://www.codeplex.com/DocProject                      *
 * http://www.codeplex.com/DocProject/Project/License.aspx *
 ***********************************************************/
using System;
using System.Collections.Generic;
using System.IO;
using DaveSexton.DocProject.Engine;

namespace DaveSexton.DocProject
{
	/// <summary>
	/// Encapsulates the settings used by classes that derive from <see cref="BuildEngine" />.
	/// </summary>
	/// <remarks>
	/// This class is not thread-safe.
	/// </remarks>
	public abstract class BuildSettings : MarshalByRefObject
	{
		#region Public Properties
		[System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Performance", "CA1802:UseLiteralsWhereAppropriate")]
		public static readonly string DefaultRelativeDocSiteDataPath = "App_Data";
		[System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Performance", "CA1802:UseLiteralsWhereAppropriate")]
		public static readonly string DefaultDocSiteContentsXmlFileName = "DocSiteContents.xml";
		[System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Performance", "CA1802:UseLiteralsWhereAppropriate")]
		public static readonly string DefaultDocSiteIndexXmlFileName = "DocSiteIndex.xml";

		public virtual string DocSiteContentsXmlFileName
		{
			get
			{
				return DefaultDocSiteContentsXmlFileName;
			}
		}

		public virtual string RelativeDocSiteContentsXmlFile
		{
			get
			{
				return Path.Combine(RelativeDocSiteDataPath, DocSiteContentsXmlFileName);
			}
		}

		public string DocSiteContentsXmlFile
		{
			get
			{
				return Path.Combine(ProjectDirectory, RelativeDocSiteContentsXmlFile);
			}
		}

		public virtual string DocSiteIndexXmlFileName
		{
			get
			{
				return DefaultDocSiteIndexXmlFileName;
			}
		}

		public virtual string RelativeDocSiteIndexXmlFile
		{
			get
			{
				return Path.Combine(RelativeDocSiteDataPath, DocSiteIndexXmlFileName);
			}
		}

		public string DocSiteIndexXmlFile
		{
			get
			{
				return Path.Combine(ProjectDirectory, RelativeDocSiteIndexXmlFile);
			}
		}

		public string ProjectOutputDirectory
		{
			get
			{
				return project.Output.OutputPath;
			}
		}

		public string RelativeProjectOutputDirectory
		{
			get
			{
				return project.Output.RelativeOutputPath;
			}
		}

		public string DocSiteTargetContentsXmlFile
		{
			get
			{
				return Path.Combine(DocSiteDataPath, DocSiteContentsXmlFileName);
			}
		}

		public string DocSiteTargetIndexXmlFile
		{
			get
			{
				return Path.Combine(DocSiteDataPath, DocSiteIndexXmlFileName);
			}
		}

		public virtual string RelativeDocSiteDataPath
		{
			get
			{
				return DefaultRelativeDocSiteDataPath;
			}
		}

		public string DocSiteDataPath
		{
			get
			{
				return Path.Combine(projectDirectory, RelativeDocSiteDataPath);
			}
		}

		/// <summary>
		/// Gets the relative path from the <see cref="ProjectDirectory" /> to a directory where a <see cref="BuildEngine" /> can build its output.
		/// </summary>
		public virtual string RelativeWorkingDirectory
		{
			get
			{
				string path = project.Output.OutputPath;

				if (string.IsNullOrEmpty(path))
					return "bin";

				Uri uri = new Uri(path, UriKind.RelativeOrAbsolute);

				if (uri.IsAbsoluteUri)
				{
					Uri projectUri = new Uri(projectDirectory);

					return System.Web.HttpUtility.UrlDecode(projectUri.MakeRelativeUri(uri).ToString());
				}
				else
					return uri.ToString();
			}
		}

		/// <summary>
		/// Gets the full path to a directory where a <see cref="BuildEngine" /> can use temporary files to build its output.
		/// </summary>
		public string WorkingDirectory
		{
			get
			{
				return Path.Combine(ProjectDirectory, RelativeWorkingDirectory);
			}
		}

		/// <summary>
		/// Gets the full path to the build output directory.
		/// </summary>
		public virtual string RelativeTargetDirectory
		{
			get
			{
				return null;
			}
		}

		/// <summary>
		/// Gets the full path to the build output directory.
		/// </summary>
		public string TargetDirectory
		{
			get
			{
				return Path.Combine(projectDirectory, RelativeTargetDirectory ?? "");
			}
		}

		/// <summary>
		/// Gets the full path to the DocProject, without the file name.
		/// </summary>
		public string ProjectDirectory
		{
			get
			{
				return projectDirectory;
			}
		}

		public virtual string RelativeWorkingXmlDocumentationPath
		{
			get
			{
				return "Comments";
			}
		}

		public string WorkingXmlDocumentationPath
		{
			get
			{
				return Path.Combine(WorkingDirectory, RelativeWorkingXmlDocumentationPath);
			}
		}

		public abstract IEnumerable<SystemPath> Targets { get; }

		/// <summary>
		/// Gets or sets the scope of the documentation being built.  The default value is <see cref="DocumentationScope" />.PublicOnly.
		/// </summary>
		/// <seealso cref="ScopeSwitch" />
		public DocumentationScope Scope
		{
			get
			{
				return engine.Options.DocumentationScope;
			}
			set
			{
				engine.Options.DocumentationScope = value;
			}
		}

		public IBuildEngine Engine
		{
			get
			{
				return engine;
			}
		}

		public IDocProject Project
		{
			get
			{
				return project;
			}
		}
		#endregion

		#region Private / Protected
		private readonly IBuildEngine engine;
		private readonly IDocProject project;
		private volatile string projectDirectory;
		#endregion

		#region Constructors
		/// <summary>
		/// Constructs a new instance of the <see cref="BuildSettings" /> class for derived classes, 
		/// initializing some of the settings based on the specified <paramref name="project" />.
		/// </summary>
		/// <strong>Note to inheritors</strong>
		/// <para>
		/// In constructors for derived classes, do not attempt to access the <see cref="DocProjectOptions"/> for the related <see cref="BuildEngine"/>.  
		/// Doing so may cause DocProject to use the <see cref="BuildEngineProvider"/> to create an instance of the <strong>DocProjectOptions</strong> 
		/// class and if the constructor for that class attempts to use the related <see cref="BuildSettings"/> then the provider may be asked 
		/// to create the settings as well, resulting in an infinite loop.
		/// </para>
		/// </remarks>
		protected BuildSettings(IBuildEngine engine)
		{
			if (engine == null)
				throw new ArgumentNullException("engine");

			this.engine = engine;

			project = engine.Project;

			// this setting is cached so that cross-thread calls do not have to be synchronized just to read it
			projectDirectory = project.Directory;
		}
		#endregion

		#region Methods
		[System.Security.Permissions.SecurityPermission(System.Security.Permissions.SecurityAction.LinkDemand,
			Flags = System.Security.Permissions.SecurityPermissionFlag.Infrastructure)]
		public override object InitializeLifetimeService()
		{
			return null;
		}

		public virtual void Refresh()
		{
			// this setting is cached so that cross-thread calls do not have to be synchronized just to read it
			projectDirectory = project.Directory;
		}

		internal IDictionary<SystemPath, IEnumerable<SystemPath>> IndexBySource(BuildContext context)
		{
			IDictionary<SystemPath, IEnumerable<SystemPath>> sources = new Dictionary<SystemPath, IEnumerable<SystemPath>>(128);

			string targetDocumentationPath = WorkingXmlDocumentationPath;

			foreach (Source source in context.Sources)
			{
				SystemPath input = source.Input;

				if (input != SystemPath.Empty && !input.Exists && !input.HasFileWildcard)
				{
					if (input.IsDirectory)
						throw new DirectoryNotFoundException(string.Format(System.Globalization.CultureInfo.CurrentCulture,
							Resources.Errors.MissingSourceDirectory, input.Path));
					else
						throw new FileNotFoundException(string.Format(System.Globalization.CultureInfo.CurrentCulture,
							Resources.Errors.MissingSourceAssemblyOrFile, input.Path));
				}

				sources[input] = ImportXmlDocumentation(source, targetDocumentationPath, context);
			}

			return sources;
		}

		private IEnumerable<SystemPath> ImportXmlDocumentation(Source source, string targetDocumentationPath, BuildContext context)
		{
			SystemPath docs = source.XmlDocumentation;

			if (docs != SystemPath.Empty)
			{
				if (!docs.IsDirectory && !docs.HasFileExtension(".xml"))
					throw new InvalidOperationException(string.Format(System.Globalization.CultureInfo.CurrentCulture, 
						Resources.Errors.InvalidSourceXmlDocumentationPathSpecified, docs.CompletePath));

				foreach (SystemPath doc in (docs.IsDirectory) ? docs.GetFiles("*.xml") : docs.GetFiles())
				{
					SystemPath target = new SystemPath(targetDocumentationPath, doc.FileName, false, false);

					if (ImportXmlDocumentationFile(doc.Path, target.Path, context, source.Input.Path))
						yield return target;
				}
			}
		}

		private static bool ImportXmlDocumentationFile(string source, string target, BuildContext context, string sourceInput)
		{
			try
			{
				if (File.Exists(source))		// no need to catch an exception if the file doesn't exist
				{
					File.Copy(source, target, true);
					return true;
				}
			}
			catch (IOException ex)
			{
				context.TraceLine();
				context.TraceLine("Error copying XML documentation file to output directory.");
				context.TraceLine(ex.GetType().FullName + ": " + ex.Message);
				context.TraceLine("Assembly or file: " + sourceInput);
				context.TraceLine("Source: " + source);
				context.TraceLine("Target: " + target);

				Log.Exception(ex, "BuildEngine: Copying XML documentation files to output directory.");
			}

			return false;
		}
		#endregion
	}
}
